<?php
/**
 * Spt_Stream_View class
 * 
 * Copyright (c) 2008, Damien Lebrun
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Damien Lebrun nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY DAMIEN LEBRUN ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL DAMIEN LEBRUN BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @category   Zend
 * @package    Spt_Stream
 * @copyright  Copyright (c) 2008 Damien Lebrun
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @author     Damien Lebrun
 */

require_once 'Zend/Cache.php';


/**
 * Stream wrapper to convert markup of mostly-PHP templates into PHP prior to include().
 * 
 * Based in large part on the example at
 * http://www.php.net/manual/en/function.stream-wrapper-register.php
 * 
 * @category   Zend
 * @package    Spt_Stream
 * @author     Mike Naberezny (@link http://mikenaberezny.com)
 * @author     Paul M. Jones  (@link http://paul-m-jones.com)
 * @author     Rob Allen (@link http://akrabat.com)
 */
class Spt_Stream_View
{
    /**
     * Protocol name
     * @var string
     */
    const PROTOCOL = 'view';
    
    /**
     * @access private
     */
    const GLOBALPATTERN = 'global';
    const TAGSPATTERN = 'tag';
    const CODEPATTERN = 'php';
    
    /**
     * cache frontend options
     * @var array
     */
    private static $_frontEndOptions = null;
    
    /**
     * cache back-end options
     * @var array
     */
    private static $_backEndOptions = null;

    /**
     * Current stream position.
     * @var int
     */
    private $_pos = 0;

    /**
     * Data for streaming.
     * @var string
     */
    private $_data;

    /**
     * Stream stats.
     * @var array
     */
    private $_stat;

    /**
     * Pattern used for replacement on the document.
     * Used the normalize php tags.
     * @var array
     */
    public static $globalPatterns = array(
        '/<\?=\s*/' => '<?php echo ', 
        '/<\?(?!php)\s*/' => '<?php ');

    /**
     * Pattern to extract php code
     * @var array
     */
    public static $embeddedCodePattern = array(
        '/(<\?php)(.*?)(\?>)/s');

    /**
     * Pattern and replacement inside php code
     * @var array
     */
    public static $patterns = array(
        '/(?<!\\\)@\$/' => '$this->', 
        '/(?<!\\\)@/' => '$this->',
        '/\\\@\$/' => '@$', 
        '/\\\@/' => '@');

    /**
     * Opens the script file and converts markup.
     * 
     * @todo allow it to handle more than one protocol
     * @param string $path
     * @return bool
     */
    public function stream_open($path)
    {
        // get the view script source
        $path = str_replace(
            self::PROTOCOL . '://', '', $path);
        
        // If reading the file failed, update our local _stat store
        // to reflect the real _stat of the file, then return on failure
        if ($this->_loadData(
            $path) === false) {
            $this->_stat = stat($path);
            return false;
        }
        
        // file_get_contents() won't update PHP's _stat cache, so performing
        // another _stat() on it will hit the filesystem again.  Since the file
        // has been successfully read, avoid this and just fake the _stat
        // so include() is happy.
        $this->_stat = array(
            'mode' => 0100777, 
            'size' => strlen($this->_data));
        return true;
    }

    /**
     * Reads from the stream.
     * 
     * @return string
     */
    public function stream_read($count)
    {
        $ret = substr($this->_data, $this->_pos, $count);
        $this->_pos += strlen($ret);
        return $ret;
    }

    /**
     * Tells the current position in the stream.
     * 
     * @return int
     */
    public function stream_tell()
    {
        return $this->_pos;
    }

    /**
     * Tells if we are at the end of the stream.
     * 
     * @return bool
     */
    public function stream_eof()
    {
        return $this->_pos >= strlen($this->_data);
    }

    /**
     * Stream statistics.
     * 
     * @return array
     */
    public function stream_stat()
    {
        return $this->_stat;
    }

    /**
     * Seek to a specific point in the stream.
     * 
     * @return bool
     */
    public function stream_seek($offset, $whence)
    {
        switch ($whence) {
            case SEEK_SET:
                if ($offset < strlen($this->_data) && $offset >= 0) {
                    $this->_pos = $offset;
                    return true;
                } else {
                    return false;
                }
                break;
            case SEEK_CUR:
                if ($offset >= 0) {
                    $this->_pos += $offset;
                    return true;
                } else {
                    return false;
                }
                break;
            case SEEK_END:
                if (strlen($this->_data) + $offset >= 0) {
                    $this->_pos = strlen(
                        $this->_data) + $offset;
                    return true;
                } else {
                    return false;
                }
                break;
            default:
                return false;
        }
    }

    /**
     * Load data form cache or source
     *
     * @param string $path
     * @return bool
     */
    private function _loadData($path)
    {
        $path = realpath($path);
        $cache = $this->_getFileCache($path);
        
        $name = md5($path);
        $this->_data = $cache->load($name);
        
        if (! $this->_data) {
            
            $this->_data = file_get_contents($path);
            
            if ($this->_data === false) {
                return false;
            }
            
            $this->_transformData();
            
            $cache->save($this->_data, $name);
        }
        
        return true;
    }

    /**
     * Apply transformation to the source.
     *
     * @return void
     */
    private function _transformData()
    {
        // Normalize opening php tag
        $this->_data = preg_replace(
            self::_getPatterns(self::GLOBALPATTERN), 
            self::_getReplacements(self::GLOBALPATTERN), $this->_data);
        
        // Transform code
        $patterns = self::_getPatterns(
            self::TAGSPATTERN);
        
        if (count($patterns) > 0) {
            $this->_data = preg_replace_callback($patterns, 
                'Spt_Stream_View::transformCode', 
                $this->_data);
        }
    }

    /**
     * Transform php code
     *
     * @todo raise exception if $matches count != 4
     * @param array $matches
     * @return string
     */
    public static function transformCode($matches)
    {
        $code = $matches[1];
        $code .= preg_replace(self::_getPatterns(self::CODEPATTERN), 
            self::_getReplacements(self::CODEPATTERN), $matches[2]);
        $code .= $matches[3];
        return $code;
    }

    /**
     * Return a cache object
     *
     * @param string $path
     * @return Zend_Cache_Front_File
     */
    private function _getFileCache($path)
    {
        self::addCacheFrontEndOption('master_file', $path);
   
        return Zend_Cache::factory('File',
            self::getCacheBackEndName(),
            self::getCacheFrontEndOptions(),
            self::getCacheBackEndOptions());
    }

    /**
     * Return patterns to replace in source 
     *
     * @return array
     */
    private static function _getPatterns($target = null)
    {
        $patterns = array();
        
        switch ($target) {
            case self::GLOBALPATTERN:
                $patterns = array_keys(
                    self::$globalPatterns);
                break;
            case self::TAGSPATTERN:
                $patterns = self::$embeddedCodePattern;
                break;
            case self::CODEPATTERN:
            default:
                $patterns = array_keys(self::$patterns);
                break;
        }
        
        return $patterns;
    }

    /**
     * Return replacement strings
     *
     * @return array
     */
    private static function _getReplacements($target = null)
    {
        $replacement = array();
        
        switch ($target) {
            case self::GLOBALPATTERN:
                $replacement = array_values(
                    self::$globalPatterns);
                break;
            case self::CODEPATTERN:
            default:
                $replacement = array_values(
                    self::$patterns);
                break;
        }
        
        return $replacement;
    }

    /**
     * Return cache back-end name - "File" by default
     * 
     * @return string
     */
    public static function getCacheBackEndName()
    {
        if (self::$_backEndOptions === null) {
            self::setCacheBackEnd();
        }
        return self::$_backEndOptions['name'];
    }
    
    /**
     * Return cache back-end options
     *
     * @return array
     */
    public static function getCacheBackEndOptions()
    {
        if (self::$_backEndOptions === null) {
            self::setCacheBackEnd();
        }
        return self::$_backEndOptions['options'];
    }

    /**
     * Return cache front-end options
     *
     * @return array
     */
    public static function getCacheFrontEndOptions()
    {
        if (self::$_frontEndOptions === null) {
            self::setCachefrontEnd();
        }
        return self::$_frontEndOptions['options'];
    }
    
    /**
     * Set cache back-end
     * @param $type string
     * @param array $options
     */
    public static function setCacheBackEnd($type = 'File', $options = array())
    {
        self::$_backEndOptions = array(
            'name' => $type,
            'options' => $options
        );
    }

    /**
     * set cache front-end
     * 
     * @param array $_frontEndOptions
     */
    public static function setCachefrontEnd($options = array())
    {
        self::$_frontEndOptions = array(
            'options' => $options
        );
    }
    
    /**
     * Set a cache back-end option
     *
     * @param string $name
     * @param mixed $value
     */
    public static function addCacheBackEndOption($name, $value)
    {
        if (self::$_backEndOptions === null) {
            self::setCacheBackEnd();
        }
    	self::$_backEndOptions['options'][$name] = $value;
    }
    
    /**
     * Set a cache front-end option
     *
     * @param unknown_type $name
     * @param unknown_type $value
     */
    public static function addCacheFrontEndOption($name, $value)
    {
        if (self::$_frontEndOptions === null) {
            self::setCachefrontEnd();
        }
    	self::$_frontEndOptions['options'][$name] = $value;
    }
}